package roboticsfmw.core;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import roboticsfmw.annotation.HardwareRef;
import roboticsfmw.annotation.OnEvent;
import roboticsfmw.annotation.TimerRef;


/**
 * @author Denys Sene
 *
 */
public class EngineRuntime extends Thread {

	private EventBus eventBus;
	private Map<String, HardwareProxy> hwMap;
	private List<Processor> processors;
	private List<Sensor<?>> sensors;
	private Map<String, Timer> timers;
	
	Logger log = Logger.getLogger(this.getClass());
	private boolean running; 	
		

	/**
	 * Constructor
	 */
	public EngineRuntime(){
		setEventBus(new EventBus());
		hwMap = new HashMap<String, HardwareProxy>();
		processors = new ArrayList<Processor>();
		sensors = new ArrayList<Sensor<?>>();
		timers = new HashMap<String, Timer>();
		setName("MasterEngine");
	}
	
	
	/**
	 * @return
	 */
	public EventBus getEventBus() {
		return eventBus;
	}

	/**
	 * @param eventBus
	 */
	public void setEventBus(EventBus eventBus) {
		this.eventBus = eventBus;
	}

	/**
	 * @param sensor
	 */
	public void registerSensor(Sensor<?> sensor) {
		sensors.add(sensor);
		sensor.setEngine(this);
		sensor.setEventBus(getEventBus());
	}
	
	
	/**
	 * @param processor
	 */
	public void registerProcessor(Processor processor) {
		processors.add(processor);
		processor.setEngine(this);
		processor.setEventBus(eventBus);
	}
	
	
	/**
	 * @param lookupKey 
	 * @param t
	 */
	public void registerTimer(String lookupKey, Timer t) {
		timers.put(lookupKey,t);
		t.setEngine(this);
		t.setEventBus(eventBus);
	}
	
	
	/**
	 * @param key
	 * @param hw
	 */
	public void registerHardware(String lookupKey, HardwareProxy hw) {
		hwMap.put(lookupKey, hw);
	}
	
	
	/**
	 * @param key
	 * @return
	 */
	public HardwareProxy lookupHardware(String key) {
		return hwMap.get(key);
	}
	
	/**
	 * @param key
	 * @return
	 */
	public Timer lookupTimer(String key) {
		return timers.get(key);
	}
	
	/**
	 * @param t
	 */
	public void startTimer(Timer t) {
		t.turnOn();
		Thread th = new Thread(t);
		th.setName(t.getName()+"-Thread");
		t.setThread(th);
		th.start();
	}

	/**
	 * @param t
	 */
	public void stopTimer(Timer t) {
		t.turnOff();
		t.reset();
		t.getThread().interrupt();
	}
	
	/**
	 * @param p
	 * @throws ProcessorException
	 */
	public void startProcessor(Processor p) throws ProcessorException {
		injectVariables(p);
		p.init();
		registerEventsForProcessor(p);
		if (p.isRunnable()) {
			p.turnOn();
			Thread t = new Thread(p);
			t.setName(p.getName()+"-Thread");
			p.setThread(t);
			t.start();
		}
	}
	
	
	/**
	 * @param p
	 * @throws ProcessorException
	 */
	public void shutdownProcessor(Processor p) throws ProcessorException {
		try {
			p.turnOff();
			if (p.isRunnable()) {
				p.getThread().interrupt();
			}
			p.shutdown();
		} catch (ProcessorException pe) {
			throw pe;
		} catch (Exception e) {
			throw new ProcessorException(e);
		}
	}
	
	
	/**
	 * @param s
	 * @throws SensorException
	 */
	public void startSensor(Sensor<? extends HardwareProxy> s) throws SensorException {
		injectVariables(s);
		s.init();
		if (s.isRunnable()) {
			s.turnOn();
			Thread t = new Thread(s);
			t.setName(s.getName()+"-Thread");
			s.setThread(t);
			t.start();
		}
	}
	
	
	/**
	 * @param s
	 * @throws SensorException
	 */
	public void shutdownSensor(Sensor<? extends HardwareProxy> s) throws SensorException {
		try {
			s.turnOff();
			s.shutdown();
			if (s.isRunnable()) {
				s.getThread().interrupt();
			}
		} catch (SensorException se) {
			throw se;
		} catch (Exception e) {
			throw new SensorException(e);
		}		
	}
	
	
	/**
	 * @param hw
	 * @throws HardwareException
	 */
	public void startHardwareProxy(HardwareProxy hw) throws HardwareException {
		try {
			hw.init();
		} catch (HardwareException e) {
			throw e;
		} catch (Exception e) {
			throw new HardwareException(e);
		}
	}

	
	public void shutdownHardwareProxy(HardwareProxy hw) throws HardwareException {
		try {
			hw.shutdown();
		} catch (HardwareException e) {
			throw e;
		} catch (Exception e) {
			throw new HardwareException(e);
		}
	}
	
	
	/**
	 * @throws HardwareException 
	 * @throws SensorException 
	 * @throws ProcessorException 
	 */
	public void startAll() throws HardwareException, SensorException, ProcessorException {
		log.info("Initializing Hardwares!");
		for (HardwareProxy hw : this.hwMap.values()) {
			startHardwareProxy(hw);
		}

		log.info("Initializing Sensors!");
		for (Sensor<?> s : this.sensors) {
			startSensor(s);
		}
		
		log.info("Starting Processors!");
		for (Processor p : this.processors) {
			startProcessor(p);
		}
		
		log.info("Starting Timers");
		for (Timer t : this.timers.values()) {
			startTimer(t);
		}

		this.running = true;
		
		// Main Engine Thread start!
		start();

		log.info("All Systems Nominal!");
	}
	

	/**
	 * Stops all system components
	 */
	private void shutdownAll() {

		log.info("Stoping Timers");
		for (Timer t : this.timers.values()) {
			stopTimer(t);
		}
		
		log.info("Stoping Processors!");
		for (Processor p : this.processors) {
			try {
				shutdownProcessor(p);
			} catch (ProcessorException e) {
				log.error("Error shuting down Processor: ",e);
			}
		}

		log.info("Stoping Sensors!");
		for (Sensor<?> s : this.sensors) {
			try {
				shutdownSensor(s);
			} catch (SensorException e) {
				log.error("Error shuting down Sensor: ",e);
			}
		}

		log.info("Disconnecting Hardwares!");
		for (HardwareProxy hw : this.hwMap.values()) {
			try {
				shutdownHardwareProxy(hw);
			} catch (HardwareException e) {
				log.error("Error disconnecting Hardware: ",e);
			}
		}

		log.info("System shutdown completed!");
	}
	
	public void waitFinish() {
		while (this.running) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
			}
		}
	}
	
	public void run() {
		waitFinish();
		shutdownAll();
	}
	
	public void shutdown() {
		this.running=false;
	}
	
	private void registerEventsForProcessor(Processor p) {
		for (Method method : p.getClass().getDeclaredMethods()) {
			if (method.isAnnotationPresent(OnEvent.class)) {
				Class<? extends Event> eventClass = method.getAnnotation(OnEvent.class).value();
				getEventBus().registerEvent(eventClass, p, method);
			}
		}
	}
	
	private void injectVariables(ManagedComponent component) {
		for (Field field : component.getClass().getDeclaredFields()) {
			Object injectedObject = null;
			if (field.isAnnotationPresent(HardwareRef.class)) {
				String hardwareName = field.getAnnotation(HardwareRef.class).name();
				injectedObject = lookupHardware(hardwareName);
				injectFieldMethod(component, field, injectedObject);
			} else if (field.isAnnotationPresent(TimerRef.class)) {
				String timerName = field.getAnnotation(TimerRef.class).name();
				injectedObject = lookupTimer(timerName);
				injectFieldMethod(component, field, injectedObject);
			}
		}	
	}
	
	private void injectFieldMethod(ManagedComponent component, Field field, Object o) {
		if ((field.getModifiers() & Modifier.PUBLIC) > 0) {
			try {
				field.set(component, o);
			} catch (Exception e) {
				log.error("Injection Error:",e);
			}
		} else {
			String oClassName = field.getName();
			String a = oClassName.substring(0, 1).toUpperCase();
			oClassName = a+oClassName.substring(1);
			try {
				Method method = component.getClass().getDeclaredMethod("set"+oClassName, o.getClass());
				method.invoke(component, o);
			} catch (Exception e) {
				log.error("Injection Error:",e);
			}
		}
		
	}
	
}
